use anyhow::{bail, Context, Result};
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};

pub struct YamlProcessor {
    source_key: String,
    dest_key: String,
}

#[derive(Debug, Default)]
pub struct ProcessResult {
    pub processed: usize,
    pub skipped: usize,
    pub failed: usize,
}

impl ProcessResult {
    pub fn total(&self) -> usize {
        self.processed + self.skipped + self.failed
    }
}

impl YamlProcessor {
    pub fn new(source_key: String, dest_key: String) -> Self {
        Self {
            source_key,
            dest_key,
        }
    }

    pub fn process_file(
        &self,
        input_path: &Path,
        output_path: Option<&Path>,
        in_place: bool,
    ) -> Result<ProcessResult> {
        self.validate_input(input_path)?;

        let content = fs::read_to_string(input_path)?;

        if content.trim().is_empty() {
            bail!("Input file is empty: {}", input_path.display());
        }

        let base_dir = input_path.parent().unwrap_or(Path::new("."));
        let (processed_content, result) = self.process_content(&content, base_dir)?;

        if result.processed > 0 {
            let output_target = if in_place {
                input_path
            } else {
                output_path.unwrap()
            };
            self.write_output(&processed_content, output_target)?;
        }

        Ok(result)
    }

    fn validate_input(&self, input_path: &Path) -> Result<()> {
        if !input_path.exists() {
            bail!("Input file not found: {}", input_path.display());
        }
        let metadata = fs::metadata(input_path)?;
        if metadata.len() > 100 * 1024 * 1024 {
            // 100MB limit
            bail!("Input file exceeds 100MB limit: {}", input_path.display());
        }
        Ok(())
    }

    fn process_content(&self, content: &str, base_dir: &Path) -> Result<(String, ProcessResult)> {
        let mut result = ProcessResult::default();
        let mut output_lines = Vec::with_capacity(content.lines().count());

        for line in content.lines() {
            output_lines.push(line.to_string());

            if line.trim().starts_with('#') {
                continue;
            }

            if let Some(file_path) = self.extract_file_path(line) {
                match self.insert_file_content(&file_path, line, base_dir) {
                    Ok(content_lines) => {
                        output_lines.extend(content_lines);
                        result.processed += 1;
                    }
                    Err(e) => {
                        eprintln!("Warning: Skipping. {}", e);
                        let is_not_found = e
                            .root_cause()
                            .downcast_ref::<std::io::Error>()
                            .map_or(false, |io| io.kind() == std::io::ErrorKind::NotFound);
                        if is_not_found {
                            result.skipped += 1;
                        } else {
                            result.failed += 1;
                        }
                    }
                }
            }
        }
        Ok((output_lines.join("\n"), result))
    }

    fn extract_file_path(&self, line: &str) -> Option<String> {
        let key_pattern = format!("{}:", self.source_key);
        line.split('#')
            .next()
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .and_then(|s| s.strip_prefix(&key_pattern))
            .map(str::trim)
            .filter(|s| !s.is_empty() && *s != "null")
            .map(|s| s.trim_matches(|c| c == '\'' || c == '"').to_string())
            .filter(|s| !s.is_empty())
    }

    fn insert_file_content(
        &self,
        file_path: &str,
        original_line: &str,
        base_dir: &Path,
    ) -> Result<Vec<String>> {
        let canonical_base_dir = base_dir.canonicalize()?;
        let full_path = if Path::new(file_path).is_absolute() {
            PathBuf::from(file_path)
        } else {
            canonical_base_dir.join(file_path)
        };

        let canonical_full_path = full_path
            .canonicalize()
            .with_context(|| format!("Source file not found at: {}", full_path.display()))?;

        if !canonical_full_path.starts_with(&canonical_base_dir) {
            bail!(
                "Security risk detected: Path traversal attempt blocked for '{}'",
                file_path
            );
        }

        let content = fs::read_to_string(&canonical_full_path)?;
        if content.trim().is_empty() {
            bail!("Source file is empty: {}", canonical_full_path.display());
        }

        let base_indent = self.detect_indent(original_line);
        let content_indent = format!("{}  ", base_indent);
        let mut lines = vec![format!("{}{}: |", base_indent, self.dest_key)];
        for content_line in content.lines() {
            lines.push(format!("{}{}", content_indent, content_line));
        }
        Ok(lines)
    }

    fn detect_indent<'a>(&self, line: &'a str) -> &'a str {
        line.find(|c: char| !c.is_whitespace())
            .map_or(line, |pos| &line[..pos])
    }

    fn write_output(&self, content: &str, path: &Path) -> Result<()> {
        let file = fs::File::create(path)?;
        let mut writer = BufWriter::new(file);
        writer.write_all(content.as_bytes())?;
        writer.flush()?;
        Ok(())
    }
}
